<

物理シミュレーションを使用してウィジェットをアニメーション化する

物理シミュレーションにより、アプリのインタラクションを現実的でインタラクティブなものにすることができます。 たとえば、ウィジェットをアニメーション化して、ウィジェットがアタッチされているかのように動作させることができます。 バネまたは重力による落下。

このレシピでは、ウィジェットをドラッグしたポイントから元のポイントに移動する方法を示します。 スプリングシミュレーションを使用して中心を合わせます。

このレシピでは次の手順を使用します。

  1. アニメーションコントローラーを設定する
  2. ジェスチャーを使用してウィジェットを移動する
  3. ウィジェットをアニメーション化する
  4. バネの動きをシミュレートする速度を計算します。

ステップ 1: アニメーション コントローラーをセットアップする

というステートフル ウィジェットから始めますDraggableCard:

import 'package:flutter/material.dart';

void main() {
  runApp(const MaterialApp(home: PhysicsCardDragDemo()));
}

class PhysicsCardDragDemo extends StatelessWidget {
  const PhysicsCardDragDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: const DraggableCard(
        child: FlutterLogo(
          size: 128,
        ),
      ),
    );
  }
}

class DraggableCard extends StatefulWidget {
  const DraggableCard({required this.child, super.key});

  final Widget child;

  @override
  State<DraggableCard> createState() => _DraggableCardState();
}

class _DraggableCardState extends State<DraggableCard> {
  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Align(
      child: Card(
        child: widget.child,
      ),
    );
  }
}

を作る_DraggableCardStateクラスの拡張元SingleTickerProviderStateMixin。 次に、アニメーションコントローラーのinitStateそしてセットvsyncthis

lib/{starter.dart → step1.dart}
@@ -29,14 +29,20 @@
29
29
  State<DraggableCard> createState() => _DraggableCardState();
30
30
  }
31
- class _DraggableCardState extends State<DraggableCard>{
31
+ class _DraggableCardState extends State<DraggableCard>
32
+ with SingleTickerProviderStateMixin {
33
+ 後期AnimationController _controller;
34
+
32
35
  @オーバーライド
33
36
  void initState() {
34
37
  super.initState();
38
+ _コントローラー =
39
+ AnimationController(vsync:this,duration:constDuration(秒:1));
35
40
  }
36
41
  @オーバーライド
37
42
  void destroy() {
43
+ _controller.dispose();
38
44
  super.dispose();
39
45
  }

ステップ 2: ジェスチャーを使用してウィジェットを移動する

ウィジェットをドラッグすると移動するようにし、位置合わせフィールドに_DraggableCardStateクラス:

lib/{step1.dart (アライメント) → step2.dart (アライメント)}
@@ -1,3 +1,4 @@
1
1
  class _DraggableCardState extends State<DraggableCard>
2
2
  with SingleTickerProviderStateMixin {
3
3
  後期AnimationController _controller;
4
+ 配置 _dragAlignment = Alignment.center;

追加ジェスチャー検出器を扱うのはonPanDownonPanUpdate、 とonPanEndコールバック。アライメントを調整するには、メディアクエリを得るために ウィジェットのサイズを求めて 2 で割ります (これにより、「ドラッグされたピクセル」の単位が に変換されます) それをコーディネートします整列を使用します。)次に、Alignウィジェットのalignment_dragAlignment:

lib/{step1.dart (ビルド) → step2.dart (ビルド)}
@@ -1,8 +1,22 @@
1
1
  @オーバーライド
2
2
  ウィジェットのビルド(BuildContext context) {
3
- 戻る 整列(
4
- 子供: カード(
5
- 子供:ウィジェット.子
3
+ 変数 サイズ = MediaQuery.of(コンテキスト).サイズ;
4
+ 戻る ジェスチャー検出器(
5
+ パンダウン:(詳細) {}
6
+ onPanUpdate: (詳細) {
7
+ setState(() {
8
+ _dragAlignment += 配置(
9
+ 詳細.デルタ.dx / (サイズ.幅 / 2)、
10
+ 詳細.delta.dy / (サイズ.高さ / 2),
11
+ );
12
+ });
13
+ }、
14
+ onPanEnd: (詳細) {}、
15
+ 子: 整列(
16
+ 配置: _dragAlignment、
17
+ 子: カード(
18
+ 子: widget.child、
19
+ )、
6
20
  )、
7
21
  );
8
22
  }

ステップ 3: ウィジェットをアニメーション化する

ウィジェットを放すと、中央に戻るはずです。

を追加Animation<Alignment>フィールドと_runAnimation方法。これ メソッドが定義するTweenウィジェットがあったポイントの間を補間します 中央の点までドラッグします。

lib/{step2.dart(アニメーション) → step3.dart(アニメーション)}
@@ -1,4 +1,5 @@
1
1
  class _DraggableCardState extends State<DraggableCard>
2
2
  with SingleTickerProviderStateMixin {
3
3
  後期AnimationController _controller;
4
+ 後期アニメーション<Alignment> _animation;
4
5
  配置 _dragAlignment = Alignment.center;
void _runAnimation() {
  _animation = _controller.drive(
    AlignmentTween(
      begin: _dragAlignment,
      end: Alignment.center,
    ),
  );
  _controller.reset();
  _controller.forward();
}

次にアップデート_dragAlignmentいつAnimationControllerを生成します 価値:

lib/{step2.dart (initState) → step3.dart (initState)}
@@ -3,4 +3,9 @@
3
3
  super.initState();
4
4
  _コントローラー =
5
5
  AnimationController(vsync:this,duration:constDuration(秒:1));
6
+ _controller.addListener(() {
7
+ setState(() {
8
+ _dragAlignment = _animation.value;
9
+ });
10
+ });
6
11
  }

次に、Alignウィジェットを使用する_dragAlignment分野:

child: Align(
  alignment: _dragAlignment,
  child: Card(
    child: widget.child,
  ),
),

最後に、更新しますGestureDetectorアニメーション コントローラーを管理するには:

lib/{step2.dart (ジェスチャー) → step3.dart (ジェスチャー)}
@@ -1,5 +1,7 @@
1
1
  return GestureDetector(
2
- onPanDown: (詳細) {}、
2
+ onPanDown: (詳細) {
3
+ _コントローラー.ストップ();
4
+ }、
3
5
  onPanUpdate: (詳細) {
4
6
  setState(() {
5
7
  _dragAlignment += 配置(
@@ -8,7 +10,9 @@
8
10
  );
9
11
  });
10
12
  }、
11
- onPanEnd: (詳細) {}、
13
+ onPanEnd: (詳細) {
14
+ _runAnimation();
15
+ }、
12
16
  子: 整列(
13
17
  配置: _dragAlignment、
14
18
  子: カード(

ステップ 4: バネの動きをシミュレートする速度を計算します。

最後のステップは、ウィジェットの速度を計算するために少し計算を行うことです。 ドラッグが終了した後。これはウィジェットを現実的にするためです スナップバックされるまでその速度で続行します。 (_runAnimation方法 アニメーションの開始位置と終了位置を設定することで、すでに方向が設定されています)。

まず、インポートしますphysicsパッケージ:

import 'package:flutter/physics.dart';

onPanEndコールバックは、ドラッグエンド詳細物体。このオブジェクト ポインタが画面との接触を停止したときのポインタの速度を示します。の 速度はピクセル/秒単位ですが、Alignウィジェットはピクセルを使用しません。それ [-1.0, -1.0] から [1.0, 1.0] までの座標値を使用します。ここで、[0.0, 0.0] 中心を表します。のsizeステップ 2 で計算されたピクセルを変換するために使用されます この範囲内の値を調整します。

ついに、AnimationController持っていますanimateWith()を与えることができるメソッド春シミュレーション:

/// Calculates and runs a [SpringSimulation].
void _runAnimation(Offset pixelsPerSecond, Size size) {
  _animation = _controller.drive(
    AlignmentTween(
      begin: _dragAlignment,
      end: Alignment.center,
    ),
  );
  // Calculate the velocity relative to the unit interval, [0,1],
  // used by the animation controller.
  final unitsPerSecondX = pixelsPerSecond.dx / size.width;
  final unitsPerSecondY = pixelsPerSecond.dy / size.height;
  final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY);
  final unitVelocity = unitsPerSecond.distance;

  const spring = SpringDescription(
    mass: 30,
    stiffness: 1,
    damping: 1,
  );

  final simulation = SpringSimulation(spring, 0, 1, -unitVelocity);

  _controller.animateWith(simulation);
}

忘れずに電話してください_runAnimation()速度とサイズを指定すると、次のようになります。

onPanEnd: (details) {
  _runAnimation(details.velocity.pixelsPerSecond, size);
},

インタラクティブな例

import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';

void main() {
  runApp(const MaterialApp(home: PhysicsCardDragDemo()));
}

class PhysicsCardDragDemo extends StatelessWidget {
  const PhysicsCardDragDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: const DraggableCard(
        child: FlutterLogo(
          size: 128,
        ),
      ),
    );
  }
}

/// A draggable card that moves back to [Alignment.center] when it's
/// released.
class DraggableCard extends StatefulWidget {
  const DraggableCard({required this.child, super.key});

  final Widget child;

  @override
  State<DraggableCard> createState() => _DraggableCardState();
}

class _DraggableCardState extends State<DraggableCard>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  /// The alignment of the card as it is dragged or being animated.
  ///
  /// While the card is being dragged, this value is set to the values computed
  /// in the GestureDetector onPanUpdate callback. If the animation is running,
  /// this value is set to the value of the [_animation].
  Alignment _dragAlignment = Alignment.center;

  late Animation<Alignment> _animation;

  /// Calculates and runs a [SpringSimulation].
  void _runAnimation(Offset pixelsPerSecond, Size size) {
    _animation = _controller.drive(
      AlignmentTween(
        begin: _dragAlignment,
        end: Alignment.center,
      ),
    );
    // Calculate the velocity relative to the unit interval, [0,1],
    // used by the animation controller.
    final unitsPerSecondX = pixelsPerSecond.dx / size.width;
    final unitsPerSecondY = pixelsPerSecond.dy / size.height;
    final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY);
    final unitVelocity = unitsPerSecond.distance;

    const spring = SpringDescription(
      mass: 30,
      stiffness: 1,
      damping: 1,
    );

    final simulation = SpringSimulation(spring, 0, 1, -unitVelocity);

    _controller.animateWith(simulation);
  }

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this);

    _controller.addListener(() {
      setState(() {
        _dragAlignment = _animation.value;
      });
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final size = MediaQuery.of(context).size;
    return GestureDetector(
      onPanDown: (details) {
        _controller.stop();
      },
      onPanUpdate: (details) {
        setState(() {
          _dragAlignment += Alignment(
            details.delta.dx / (size.width / 2),
            details.delta.dy / (size.height / 2),
          );
        });
      },
      onPanEnd: (details) {
        _runAnimation(details.velocity.pixelsPerSecond, size);
      },
      child: Align(
        alignment: _dragAlignment,
        child: Card(
          child: widget.child,
        ),
      ),
    );
  }
}